﻿Public Class TreeNode : Implements IComparable

    Public Nodes As TreeNodes
    Dim DashedPen As New Pen(Brushes.Gray) With {.DashStyle = Drawing2D.DashStyle.Dot}

    Sub New(ByVal Tree As TreeView, ByVal Parent As TreeNode)
        Me.Tree = Tree
        Me.Nodes = New TreeNodes(Tree, Me)
        If Parent Is Nothing Then
            Tree.Nodes.Add(Me)
        Else
            Parent.Nodes.Add(Me)
        End If
        _Parent = Parent
    End Sub

    Sub New(ByVal Tree As TreeView, ByVal Parent As TreeNode, ByVal Index As Integer)
        Me.Tree = Tree
        _Parent = Parent
        Me.Nodes = New TreeNodes(Tree, Me)
        Owner.Insert(Index, Me)
    End Sub

    ReadOnly Property Owner As TreeNodes
        Get
            Return If(Parent, Tree).Nodes
        End Get
    End Property

    Public Tree As TreeView

    ReadOnly Property PrevNode As TreeNode
        Get
            If Owner.First Is Me Then Return Nothing
            Return Owner(Owner.IndexOf(Me) - 1)
        End Get
    End Property

    Dim _Image As Image
    Property Image As Image
        Get
            If Tree.AutoImage Then Return If(Nodes.Count = 0, Tree.ImageLeaf, Tree.ImageFolder)
            Return _Image
        End Get
        Set(ByVal Value As Image)
            _Image = Value
            MustMeasure = True
            Tree.Invalidate()
        End Set
    End Property

    Dim MustMeasure As Boolean

    ReadOnly Property IsEditing As Boolean
        Get
            If Tree.EditBox Is Nothing Then Return False
            Return Tree.EditBox.Visible
        End Get
    End Property

    ReadOnly Property Level As Integer
        Get
            Dim i As Integer = 0
            Dim Node = Parent
            Do Until Node Is Nothing
                Node = Node.Parent
                i += 1
            Loop
            Return i
        End Get
    End Property

    Dim _Text As String
    Property Text As String
        Get
            Return _Text
        End Get
        Set(ByVal Value As String)
            _Text = Value
            MustMeasure = True
        End Set
    End Property

    ReadOnly Property LastNode As Object
        Get
            Return Nodes.Last
        End Get
    End Property

    ReadOnly Property Index As Integer
        Get
            Return Owner.IndexOf(Me)
        End Get
    End Property

    ReadOnly Property NextNode As TreeNode
        Get
            If Owner.Last Is Me Then Return Nothing
            Return Owner(Owner.IndexOf(Me) + 1)
        End Get
    End Property

    Property ForeColor As Object

    Property Height As Integer

    Property Expanded As Boolean

    Private ReadOnly Property Font As Font
        Get
            Return Tree.Font
        End Get
    End Property

    Sub BeginEdit()
        If Not Tree.LabelEdit Then Exit Sub
        Dim R As Rectangle = TextRect
        R.Inflate(0, -2)
        R.Offset(0, -Tree.ScrollBar.Value)
        Tree.EditBox.Location = R.Location
        Tree.EditBox.Size = R.Size
        Tree.EditBox.MaxWidth = Tree.Width - Tree.ScrollBar.Width - R.Left
        Tree.EditBox.OriginalText = Text
        Tree.EditBox.Text = "*"
        Tree.EditBox.Text = Text
        Tree.EditBox.Visible = True
        Tree.EditBox.Focus()
        Tree.Invalidate()
    End Sub

    Sub Remove()
        If Me Is Owner.Last Then SelectPrev() Else CollapseAll() : SelectNext()
        Owner.Remove(Me)
        _Parent = Nothing
        Tree.Invalidate()
    End Sub

    Dim Rect As Rectangle
    Dim LineRect As Rectangle
    Dim PlusRect As Rectangle
    Friend ImageRect As Rectangle
    Friend TextRect As Rectangle
    Dim InsertBefore As Rectangle
    Dim InsertAfter As Rectangle

    Sub Measure(ByVal G As Graphics, ByVal x As Integer, ByRef y As Integer, ByVal Width As Integer)
        LineRect = New Rectangle(0, y, Width, Height)
        Rect = New Rectangle(x, y, Width - x, Height)
        PlusRect = New Rectangle(Rect.X, Rect.Y, 18, Height)
        ImageRect = New Rectangle(PlusRect.Left + Tree.Indentation + 2, Rect.Y, Tree.ImageSize.Width, Height)
        TextRect = New Rectangle(ImageRect.Right, Rect.Y, TextRect.Width, Height)
        If MustMeasure Or Tree.MustMeasure Then
            Dim RectSize = TextRenderer.MeasureText(If(Text = "", " ", Text), Font, New Size(Tree.Width - Tree.ScrollBar.Width - TextRect.Left, 0), TextFormatFlags.WordBreak)
            Height = RectSize.Height + 4
            TextRect.Width = RectSize.Width
            Rect.Height = Height
            LineRect.Height = Height
            TextRect.Height = Height
        End If
        PlusRect.Height = Height
        If Not Image Is Nothing Then
            ImageRect.Height = Height
            ImageRect.Inflate(0, -(Height - Tree.ImageSize.Height) \ 2)
            ImageRect.Height = Tree.ImageSize.Height
        End If
        InsertBefore = Nothing
        InsertAfter = Nothing
        InsertBefore = New Rectangle(ImageRect.Left - 7, y - 1, 0, 0)
        InsertBefore.Inflate(7, 5)
        y += Height + 1
        If Me Is Owner.Last Then
            InsertAfter = New Rectangle(ImageRect.Left - 7, y - 1, 0, 0)
            InsertAfter.Inflate(7, 5)
        End If
        If Expanded Then Nodes.Measure(G, x + Tree.Indentation, y, Width)
        MustMeasure = False
    End Sub

    Sub DrawLines(ByVal G As Graphics, ByVal VisibleArea As Rectangle)
        If Not Parent Is Nothing AndAlso Parent.Rect.Top > VisibleArea.Bottom Then Exit Sub
        If Not NextNode Is Nothing AndAlso NextNode.Rect.Bottom < VisibleArea.Top Then Exit Sub
        Dim y = PlusRect.Top + PlusRect.Height \ 2
        Dim x = PlusRect.Left + PlusRect.Width \ 2
        G.DrawLine(DashedPen, x, y, ImageRect.Left, y)
        If Not PrevNode Is Nothing Then
            G.DrawLine(DashedPen, x, y, x, PrevNode.Rect.Top + PrevNode.Rect.Height \ 2)
        ElseIf Not Parent Is Nothing Then
            G.DrawLine(DashedPen, x, y, x, Parent.ImageRect.Bottom)
        End If
        If Not Expanded Then Exit Sub
        Nodes.DrawLines(G, VisibleArea)
    End Sub

    Sub Draw(ByVal G As Graphics, ByVal VisibleArea As Rectangle)
        If LineRect.IntersectsWith(VisibleArea) Then
            If Tree.SelectedNode Is Me Then
                If Not IsEditing Then
                    If Tree.Focused Or Tree.EditBox.Focused Then
                        G.FillRectangle(SystemBrushes.Highlight, TextRect)
                        ControlPaint.DrawFocusRectangle(G, TextRect)
                    ElseIf Tree.EditBox.Focused Then
                        G.FillRectangle(New SolidBrush(Tree.Highlight), TextRect)
                        ControlPaint.DrawFocusRectangle(G, TextRect)
                    Else
                        G.FillRectangle(SystemBrushes.Control, TextRect)
                    End If
                End If
            ElseIf Tree.DropNode Is Me Then
                If Tree.DropRect = TextRect Or Tree.DropRect = ImageRect Then
                    G.FillRectangle(New SolidBrush(SystemColors.HotTrack), TextRect)
                ElseIf Tree.DropRect = PlusRect Then
                    G.FillRectangle(New SolidBrush(SystemColors.HotTrack), TextRect)
                    If Not Expanded And Nodes.Count > 0 Then EnsureExpanded()
                ElseIf Tree.DropRect = InsertBefore Then
                    G.FillRectangle(New SolidBrush(SystemColors.HotTrack), InsertBefore)
                ElseIf Tree.DropRect = InsertAfter Then
                    G.FillRectangle(New SolidBrush(SystemColors.HotTrack), InsertAfter)
                End If
            End If
            If Nodes.Count > 0 Then
                PlusRect.Inflate(2, 2)
                If Not Expanded Then
                    Dim v As New VisualStyles.VisualStyleRenderer(VisualStyles.VisualStyleElement.TreeView.Glyph.Closed)
                    v.DrawBackground(G, PlusRect)
                Else
                    Dim v As New VisualStyles.VisualStyleRenderer(VisualStyles.VisualStyleElement.TreeView.Glyph.Opened)
                    v.DrawBackground(G, PlusRect)
                End If
            End If
            If Not Image Is Nothing Then
                G.InterpolationMode = Drawing2D.InterpolationMode.HighQualityBicubic
                G.DrawImage(Image, ImageRect)
            End If
            Dim F As New StringFormat
            F.LineAlignment = StringAlignment.Center
            If Not (Tree.SelectedNode Is Me AndAlso IsEditing) Then
                Dim Col = SystemColors.WindowText
                If Tree.Focused AndAlso (Tree.SelectedNode Is Me Or (Tree.DropNode Is Me And (Tree.DropRect = TextRect Or Tree.DropRect = ImageRect))) Then Col = SystemColors.HighlightText
                TextRenderer.DrawText(G, Text, Font, TextRect, Col, Color.Transparent, TextFormatFlags.Left Or TextFormatFlags.VerticalCenter Or TextFormatFlags.WordBreak Or TextFormatFlags.PreserveGraphicsTranslateTransform)
            End If
        End If
        If Expanded Then Nodes.Draw(G, VisibleArea)
    End Sub

    Function ProcessMouse(ByVal P As Point) As Boolean
        If PlusRect.Contains(P) Then
            Expanded = Not Expanded
            Tree.Invalidate()
            Return True
        End If
        If LineRect.Contains(P) Then
            Tree.SelectedNode = Me
            Tree.Invalidate()
            Return True
        End If
        If Not Expanded Then Return False
        Return Nodes.ProcessMouse(P)
        For Each Node In Nodes
            If Node.ProcessMouse(P) Then Return True
        Next
        Return False
    End Function

    Sub SelectNext()
        If Tree.SelectedNode Is Me And Expanded And Nodes.Count > 0 Then
            Tree.SelectedNode = Nodes.First
            Exit Sub
        End If
        If Not Owner.Last Is Me Then
            Tree.SelectedNode = NextNode
            Exit Sub
        ElseIf Not Parent Is Nothing Then
            Parent.SelectNext()
        End If
    End Sub

    Sub SelectPrev()
        If Owner.First Is Me Then
            If Parent Is Nothing Then Exit Sub
            Tree.SelectedNode = Parent
        Else
            PrevNode.SelectLast()
        End If
    End Sub

    Sub SelectLast()
        If Not Expanded Or Nodes.Count = 0 Then
            Tree.SelectedNode = Me
        Else
            Nodes.Last.SelectLast()
        End If
    End Sub

    Sub EnsureVisible()
        If Not Parent Is Nothing Then Parent.EnsureExpanded()
        Tree.Refresh()
        If Rect.Top - Tree.ScrollBar.Value < 0 Then
            Tree.ScrollBar.Value = Math.Max(0, Rect.Top - 4)
        ElseIf Rect.Bottom > Tree.Height + Tree.ScrollBar.Value Then
            Tree.ScrollBar.Value = Rect.Bottom - Tree.Height + 4
        End If
    End Sub

    Sub ExpandAll()
        Expanded = True
        Nodes.ExpandAll()
        Tree.Invalidate()
    End Sub

    Sub CollapseAll()
        Expanded = False
        Nodes.CollapseAll()
        Tree.Invalidate()
    End Sub

    Private Sub EnsureExpanded()
        Expanded = True
        If Parent Is Nothing Then Exit Sub
        Parent.EnsureExpanded()
    End Sub

    Dim _Parent As TreeNode
    Property Parent As TreeNode
        Get
            Return _Parent
        End Get
        Set(ByVal Value As TreeNode)
            If Not _Parent Is Nothing Then Owner.Remove(Me)
            _Parent = Value
            If Not Owner.Contains(Me) Then Owner.Add(Me)
            Tree.Invalidate()
        End Set
    End Property

    Sub EndEdit()
        Tree.EditBox.Visible = False
        Tree.Invalidate()
        Tree.Focus()
    End Sub

    Function HitTest(ByVal P As Point) As TreeView.HitTestInfo
        Dim Info As New TreeView.HitTestInfo With {.Node = Me}
        If ImageRect.Contains(P) Then Info.Rectangle = ImageRect : Return Info
        If TextRect.Contains(P) Then Info.Rectangle = TextRect : Return Info
        If InsertBefore.Contains(P) Then Info.Rectangle = InsertBefore : Return Info
        If InsertAfter.Contains(P) Then Info.Rectangle = InsertAfter : Return Info
        If Expanded Then Return Nodes.HitTest(P)
        Return Nothing
    End Function

    Sub [Select]()
        Tree.SelectedNode = Me
    End Sub

    Function Contains(ByVal Node As TreeNode) As Boolean
        If Me Is Node Then Return True
        If Parent Is Nothing Then Return False
        Return Parent.Contains(Node)
    End Function

    Sub MoveDown()
        Dim NextNode = Me.NextNode
        If NextNode Is Nothing Then Exit Sub
        Dim Owner = Me.Owner
        Remove()
        Owner.Insert(NextNode.Index + 1, Me)
    End Sub

    Sub MoveUp()
        Dim PrevNode = Me.PrevNode
        If PrevNode Is Nothing Then Exit Sub
        Dim Owner = Me.Owner
        Remove()
        Owner.Insert(PrevNode.Index, Me)
    End Sub

    Sub Sort()
        Nodes.Sort()
        Tree.Invalidate()
    End Sub

    Public Function CompareTo(ByVal obj As Object) As Integer Implements System.IComparable.CompareTo
        Dim Node = CType(obj, TreeNode)
        Return StrComp(Text, Node.Text, CompareMethod.Text)
    End Function

    Sub Dropped(ByVal Node As TreeNode)
        If Tree.DropRect = TextRect Or Tree.DropRect = ImageRect Then
            Expanded = True
            Node.Parent = Me
        ElseIf Tree.DropRect = InsertBefore Then
            Node.Remove()
            Owner.Insert(Me.Index, Node)
            Node.Parent = Parent
        ElseIf Tree.DropRect = InsertAfter Then
            Node.Remove()
            Owner.Insert(Me.Index + 1, Node)
            Node.Parent = Parent
        End If
        Node.Select()
    End Sub

End Class